use std::{fs, path::Path, sync::mpsc, time::Instant};

use cow_utils::CowUtils;
use rayon::prelude::*;

use oxc_diagnostics::{DiagnosticSender, DiagnosticService};

use super::command::OutputOptions;
use crate::core::{FormatFileSource, FormatResult, SourceFormatter, utils};

pub enum SuccessResult {
    Changed(String),
    Unchanged,
}

pub struct FormatService {
    cwd: Box<Path>,
    output_options: OutputOptions,
    formatter: SourceFormatter,
}

impl FormatService {
    pub fn new<T>(cwd: T, output_options: OutputOptions, formatter: SourceFormatter) -> Self
    where
        T: Into<Box<Path>>,
    {
        Self { cwd: cwd.into(), output_options, formatter }
    }

    /// Process entries as they are received from the channel
    pub fn run_streaming(
        &self,
        rx_entry: mpsc::Receiver<FormatFileSource>,
        tx_error: &DiagnosticSender,
        tx_success: &mpsc::Sender<SuccessResult>,
    ) {
        rx_entry.into_iter().par_bridge().for_each(|entry| {
            let start_time = Instant::now();

            let path = entry.path();
            let Ok(source_text) = utils::read_to_string(path) else {
                // This happens if binary file is attempted to be formatted
                // e.g. `.ts` for MPEG-TS video file
                let diagnostics = DiagnosticService::wrap_diagnostics(
                    self.cwd.clone(),
                    path,
                    "",
                    vec![
                        oxc_diagnostics::OxcDiagnostic::error(format!(
                            "Failed to read file: {}",
                            path.display()
                        ))
                        .with_help("This may be due to the file being a binary or inaccessible."),
                    ],
                );
                tx_error.send(diagnostics).unwrap();
                return;
            };

            let (code, is_changed) = match self.formatter.format(&entry, &source_text) {
                FormatResult::Success { code, is_changed } => (code, is_changed),
                FormatResult::Error(diagnostics) => {
                    let errors = DiagnosticService::wrap_diagnostics(
                        self.cwd.clone(),
                        path,
                        &source_text,
                        diagnostics,
                    );
                    tx_error.send(errors).unwrap();
                    return;
                }
            };

            let elapsed = start_time.elapsed();

            // Write back if needed
            if matches!(self.output_options, OutputOptions::Write) && is_changed {
                fs::write(path, code)
                    .map_err(|_| format!("Failed to write to '{}'", path.to_string_lossy()))
                    .unwrap();
            }

            // Report result
            let result = match (&self.output_options, is_changed) {
                (OutputOptions::Check | OutputOptions::ListDifferent, true) => {
                    let display_path = path
                        // Show path relative to `cwd` for cleaner output
                        .strip_prefix(&self.cwd)
                        .unwrap_or(path)
                        .to_string_lossy()
                        // Normalize path separators for consistent output across platforms
                        .cow_replace('\\', "/")
                        .to_string();
                    let elapsed = elapsed.as_millis();

                    if matches!(self.output_options, OutputOptions::Check) {
                        SuccessResult::Changed(format!("{display_path} ({elapsed}ms)"))
                    } else {
                        SuccessResult::Changed(display_path)
                    }
                }
                _ => SuccessResult::Unchanged,
            };
            tx_success.send(result).unwrap();
        });
    }
}
